package threading;

// 1.自己实现一个阻塞队列
// 2.保证线程安全就是加锁
// 3.阻塞（为空时出队列阻塞，满时入队列阻塞）

// 自己写一个阻塞队列的类，名字还是不要和标准库的相同
class MyBlockingQueue {
    // 使用数组实现一个普通队列（循环队列）
    private int[] items = new int[1000];

    // 给以下的变量也加上volatile来保证内存的可见性
    // 指针
    private volatile int head = 0;
    private volatile int tail = 0;
    // 如何判断满，那就使用一个变量来记录元素个数
    private volatile int size = 0;

    // 因为涉及到修改操作，此时多线程的话就会存在线程不安全问题，所以就要加锁保证线程安全！

    // 入队列+
    public void put(int elem) {
        synchronized (this) { // 加锁对象直接this就行
            // 判定队列是否为满，满了就不能插入
            // 2. 此时队列为满，不能继续入队，就进行阻塞等待
            // 为防止等待之后出现条件不满足，循环判断
            //if(size >= items.length) {
            while (size >= items.length) {
                // return; // 满了插入不了
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 2. 那么啥时候会唤醒呢，就是该队列中元素不满的时候去唤醒，唤醒后继续往后执行
            }
            // 没满就进行插入操作：把elem元素放到数组items中tail指向的位置
            items[tail] = elem;
            tail++;
            // 循环队列 判断是否满了需要折回来(但是因为前面有size的判断，所以即使此时需要折回来，但是也并不说明队列已满）
            if(tail >= items.length) {
                tail = 0;
            }

            // 上述循环的代码 还可以直接写为
            // tail = tail % items.length; // 同样实现了循环
            // 虽然效果一样，但是这里不建议这么写，因为不直观+运行速度也没有优势
            size++;

            // 元素插入成功之后 进行唤醒wait
            this.notify();
        }

    }

    // 出队列 返回删除的元素
    public Integer take() throws InterruptedException {
        synchronized (this) {
            // 判断队列是否为空，为空就不能进行删除
            // 这里同样进行while重判断：
            //if(size == 0) {
            while (size == 0) {
                // return null;  //  为了此时可以返回null，在写该方法时返回值写为Integer而不是int！

                // 队列为空，就不能出队，进行阻塞等待
                /*try {
                    this.wait(); // 对象是this
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                this.wait();
                // 在阻塞等待后啥时候去唤醒呢，也就是在插入成功队列中有元素之后
            }
            // 如果非空，就进行出队列并删除，出的是队首
            int ret = items[head];
            head++;
            // 判断head是不是最后一个要进行循环回第一个
            if(head >= items.length) {
                head = 0;
            }
            size--;

            // 2. 此时队列不满，可以再进行入队操作了，就唤醒wait
            this.notify();
            return ret;
        }
    }

    // 注意：在队列中，同一个队列同一时刻一般不会出现既是空又是满的情况
    // 也就是说put和take一般是不会同时阻塞的，这两个相互排斥

    // 但是会存在一个问题：就是说进入wait阻塞等待状态之后是要经历一段时间才会被唤醒的，
    // 而这个等待时间要经历“沧海桑田”，此时被唤醒之后就一定是满足条件的非空/非满了吗？
    // 其实这是不一定的，所以说：最稳妥的做法应该是
    // 在【唤醒之后】也要【再判断】一下条件进行条件的重新确认！！
    // 直接使用while循环进行条件判断！！
    // 这样的话即使在唤醒后也是重新进行条件判断，只有在满足之后才会继续向后执行，否则就继续等待！
    // 因为这里的wait并不一定是另一个线程的put唤醒的，也有可能是其他方式唤醒的，如interrupt
    // 如果是这样的话，唤醒之后可能条件还不成熟去继续执行。

    // 注意：唤醒是要在另一个线程中进行唤醒，同一个线程中不可以对同一个对象又是wait又是notify！！


    // 获取队首元素

}

public class Demo2 {
    // 生产者-消费者模型
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();

        // 生产者
        Thread producer = new Thread(() -> {
            int n = 1;
            while (true) {
                try {
                    queue.put(n);
                    System.out.println("生产元素 " +n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 消费者
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    int n = queue.take();
                    System.out.println("消费元素 " +n);
                    Thread.sleep(500); // 这里也可以加休眠时间，当然不加也ok
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        consumer.start();

        // 生产一个消费一个！
    }
}
